/*
 * File: iframeResizer.contentWindow.js
 * Desc: Include this file in any page being loaded into an iframe
 *       to force the iframe to resize to the content size.
 * Requires: iframeResizer.js on host page.
 * Author: David J. Bradshaw - dave@bradshaw.net
 * Contributor: Jure Mav - jure.mav@gmail.com
 */

;(function() {
	'use strict';

	var
		autoResize            = true,
		base                  = 10,
		bodyBackground        = '',
		bodyMargin            = 0,
		bodyMarginStr         = '',
		bodyPadding           = '',
		calculateWidth        = false,
		doubleEventList       = {'resize':1,'click':1},
		eventCancelTimer      = 128,
		height                = 1,
		firstRun              = true,
		heightCalcModeDefault = 'offset',
		heightCalcMode        = heightCalcModeDefault,
		initLock              = true,
		initMsg               = '',
		inPageLinks           = {},
		interval              = 32,
		logging               = false,
		msgID                 = '[iFrameSizer]',  //Must match host page msg ID
		msgIdLen              = msgID.length,
		myID                  = '',
		publicMethods         = false,
		resetRequiredMethods  = {max:1,scroll:1,bodyScroll:1,documentElementScroll:1},
		targetOriginDefault   = '*',
		target                = window.parent,
		tolerance             = 0,
		triggerLocked         = false,
		triggerLockedTimer    = null,
		width                 = 1;


	function addEventListener(el,evt,func){
		if ('addEventListener' in window){
			el.addEventListener(evt,func, false);
		} else if ('attachEvent' in window){ //IE
			el.attachEvent('on'+evt,func);
		}
	}

	function formatLogMsg(msg){
		return msgID + '[' + myID + ']' + ' ' + msg;
	}

	function log(msg){
		if (logging && ('object' === typeof window.console)){
			console.log(formatLogMsg(msg));
		}
	}

	function warn(msg){
		if ('object' === typeof window.console){
			console.warn(formatLogMsg(msg));
		}
	}


	function init(){
		log('Initialising iFrame');
		readData();
		setMargin();
		setBodyStyle('background',bodyBackground);
		setBodyStyle('padding',bodyPadding);
		injectClearFixIntoBodyElement();
		checkHeightMode();
		stopInfiniteResizingOfIFrame();
		setupPublicMethods();
		startEventListeners();
		inPageLinks = setupInPageLinks();
		sendSize('init','Init message from host page');
	}

	function readData(){

		var data = initMsg.substr(msgIdLen).split(':');

		function strBool(str){
			return 'true' === str ? true : false;
		}

		myID               = data[0];
		bodyMargin         = (undefined !== data[1]) ? Number(data[1])   : bodyMargin; //For V1 compatibility
		calculateWidth     = (undefined !== data[2]) ? strBool(data[2])  : calculateWidth;
		logging            = (undefined !== data[3]) ? strBool(data[3])  : logging;
		interval           = (undefined !== data[4]) ? Number(data[4])   : interval;
		publicMethods      = (undefined !== data[5]) ? strBool(data[5])  : publicMethods;
		autoResize         = (undefined !== data[6]) ? strBool(data[6])  : autoResize;
		bodyMarginStr      = data[7];
		heightCalcMode     = (undefined !== data[8]) ? data[8]           : heightCalcMode;
		bodyBackground     = data[9];
		bodyPadding        = data[10];
		tolerance          = (undefined !== data[11]) ? Number(data[11]) : tolerance;
		inPageLinks.enable = (undefined !== data[12]) ? strBool(data[12]): false;
	}

	function chkCSS(attr,value){
		if (-1 !== value.indexOf('-')){
			warn('Negative CSS value ignored for '+attr);
			value='';
		}
		return value;
	}

	function setBodyStyle(attr,value){
		if ((undefined !== value) && ('' !== value) && ('null' !== value)){
			document.body.style[attr] = value;
			log('Body '+attr+' set to "'+value+'"');
		}
	}

	function setMargin(){
		//If called via V1 script, convert bodyMargin from int to str
		if (undefined === bodyMarginStr){
			bodyMarginStr = bodyMargin+'px';
		}
		chkCSS('margin',bodyMarginStr);
		setBodyStyle('margin',bodyMarginStr);
	}

	function stopInfiniteResizingOfIFrame(){
		document.documentElement.style.height = '';
		document.body.style.height = '';
		log('HTML & body height set to "auto"');
	}


	function addTriggerEvent(options){
		function addListener(eventName){
			addEventListener(window,eventName,function(e){
				sendSize(options.eventName,options.eventType);
			});
		}

		if(options.eventNames && Array.prototype.map){
			options.eventName = options.eventNames[0];
			options.eventNames.map(addListener);
		} else {
			addListener(options.eventName);
		}

		log('Added event listener: ' + options.eventType);
	}

	function initEventListeners(){
		addTriggerEvent({ eventType: 'Animation Start',           eventNames: ['animationstart','webkitAnimationStart'] });
		addTriggerEvent({ eventType: 'Animation Iteration',       eventNames: ['animationiteration','webkitAnimationIteration'] });
		addTriggerEvent({ eventType: 'Animation End',             eventNames: ['animationend','webkitAnimationEnd'] });
		addTriggerEvent({ eventType: 'Device Orientation Change', eventName:  'deviceorientation' });
		addTriggerEvent({ eventType: 'Transition End',            eventNames: ['transitionend','webkitTransitionEnd','MSTransitionEnd','oTransitionEnd','otransitionend'] });
		addTriggerEvent({ eventType: 'Window Clicked',            eventName:  'click' });
		//addTriggerEvent({ eventType: 'Window Mouse Down',         eventName:  'mousedown' });
		//addTriggerEvent({ eventType: 'Window Mouse Up',           eventName:  'mouseup' });
		addTriggerEvent({ eventType: 'Window Resized',            eventName:  'resize' });
	}

	function checkHeightMode(){
		if (heightCalcModeDefault !== heightCalcMode){
			if (!(heightCalcMode in getHeight)){
				warn(heightCalcMode + ' is not a valid option for heightCalculationMethod.');
				heightCalcMode='bodyScroll';
			}
			log('Height calculation method set to "'+heightCalcMode+'"');
		}
	}

	function startEventListeners(){
		if ( true === autoResize ) {
			initEventListeners();
			setupMutationObserver();
		}
		else {
			log('Auto Resize disabled');
		}
	}

	function injectClearFixIntoBodyElement(){
		var clearFix = document.createElement('div');
		clearFix.style.clear = 'both';
		clearFix.style.display = 'block'; //Guard against this having been globally redefined in CSS.
		document.body.appendChild(clearFix);
	}

	function setupInPageLinks(){

		function getPagePosition (){
			return {
				x: (window.pageXOffset !== undefined) ? window.pageXOffset : document.documentElement.scrollLeft,
				y: (window.pageYOffset !== undefined) ? window.pageYOffset : document.documentElement.scrollTop
			};
		}

		function getElementPosition(el){
			var
				elPosition   = el.getBoundingClientRect(),
				pagePosition = getPagePosition();

			return {
				x: parseInt(elPosition.left,10) + parseInt(pagePosition.x,10),
				y: parseInt(elPosition.top,10)  + parseInt(pagePosition.y,10)
			};
		}

		function findTarget(location){
			var hash = location.split("#")[1] || "";
			var hashData = decodeURIComponent(hash);

			function jumpToTarget(target){
				var jumpPosition = getElementPosition(target);

				log('Moving to in page link (#'+hash+') at x: '+jumpPosition.x+' y: '+jumpPosition.y);
				sendMsg(jumpPosition.y, jumpPosition.x, 'scrollToOffset'); // X&Y reversed at sendMsg uses height/width
			}

			var target = document.getElementById(hashData) || document.getElementsByName(hashData)[0];

			if (target){
				jumpToTarget(target);
			} else {
				log('In page link (#' + hash + ') not found in iFrame, so sending to parent');
				sendMsg(0,0,'inPageLink','#'+hash);
			}
		}

		function checkLocationHash(){
			if ('' !== location.hash && '#' !== location.hash){
				findTarget(location.href);
			}
		}

		function bindAnchors(){
			function setupLink(el){
				function linkClicked(e){
					e.preventDefault();

					/*jshint validthis:true */
					findTarget(this.getAttribute('href'));
				}

				if ('#' !== el.getAttribute('href')){
					addEventListener(el,'click',linkClicked);
				}
			}

			Array.prototype.forEach.call( document.querySelectorAll( 'a[href^="#"]' ), setupLink );
		}

		function bindLocationHash(){
			addEventListener(window,'hashchange',checkLocationHash);
		}

		function initCheck(){ //check if page loaded with location hash after init resize
			setTimeout(checkLocationHash,eventCancelTimer);
		}

		function enableInPageLinks(){
			if(Array.prototype.forEach && document.querySelectorAll){
				log('Setting up location.hash handlers');
				bindAnchors();
				bindLocationHash();
				initCheck();
			} else {
				warn('In page linking not fully supported in this browser! (See README.md for IE8 workaround)');
			}
		}

		if(inPageLinks.enable){
			enableInPageLinks();
		} else {
			log('In page linking not enabled');
		}

		return {
			findTarget:findTarget
		};
	}

	function setupPublicMethods(){
		if (publicMethods) {
			log('Enable public methods');

			window.parentIFrame = {
				close: function closeF(){
					sendSize('close','parentIFrame.close()', 0, 0);
				},
				getId: function getIdF(){
					return myID;
				},
				moveToAnchor: function moveToAnchorF(hash){
					inPageLinks.findTarget(hash);
				},
				reset: function resetF(){
					resetIFrame('parentIFrame.reset');
				},
				scrollTo: function scrollToF(x,y){
					sendMsg(y,x,'scrollTo'); // X&Y reversed at sendMsg uses height/width
				},
				scrollToOffset: function scrollToF(x,y){
					sendMsg(y,x,'scrollToOffset'); // X&Y reversed at sendMsg uses height/width
				},
				sendMessage: function sendMessageF(msg,targetOrigin){
					sendMsg(0,0,'message',JSON.stringify(msg),targetOrigin);
				},
				setHeightCalculationMethod: function setHeightCalculationMethodF(heightCalculationMethod){
					heightCalcMode = heightCalculationMethod;
					checkHeightMode();
				},
				setTargetOrigin: function setTargetOriginF(targetOrigin){
					log('Set targetOrigin: '+targetOrigin);
					targetOriginDefault = targetOrigin;
				},
				size: function sizeF(customHeight, customWidth){
					var valString = ''+(customHeight?customHeight:'')+(customWidth?','+customWidth:'');
					lockTrigger();
					sendSize('size','parentIFrame.size('+valString+')', customHeight, customWidth);
				}
			};
		}
	}

	function initInterval(){
		if ( 0 !== interval ){
			log('setInterval: '+interval+'ms');
			setInterval(function(){
				sendSize('interval','setInterval: '+interval);
			},Math.abs(interval));
		}
	}

	function setupInjectElementLoadListners(mutations){
		function addLoadListener(element){
			if (element.height === undefined || element.width === undefined || 0 === element.height || 0 === element.width){
				log('Attach listerner to '+element.src);
				addEventListener(element,'load', function imageLoaded(){
					sendSize('imageLoad','Image loaded');
				});
			}
		}

		mutations.forEach(function (mutation) {
			if (mutation.type === 'attributes' && mutation.attributeName === 'src'){
				addLoadListener(mutation.target);
			} else if (mutation.type === 'childList'){
				var images = mutation.target.querySelectorAll('img');
				Array.prototype.forEach.call(images,function (image) {
					addLoadListener(image);
				});
			}
		});
	}

	function setupMutationObserver(){

		var MutationObserver = window.MutationObserver || window.WebKitMutationObserver;

		function createMutationObserver(){
			var
				target = document.querySelector('body'),

				config = {
					attributes            : true,
					attributeOldValue     : false,
					characterData         : true,
					characterDataOldValue : false,
					childList             : true,
					subtree               : true
				},

				observer = new MutationObserver(function(mutations) {
					sendSize('mutationObserver','mutationObserver: ' + mutations[0].target + ' ' + mutations[0].type);
					setupInjectElementLoadListners(mutations); //Deal with WebKit asyncing image loading when tags are injected into the page
				});

			log('Enable MutationObserver');
			observer.observe(target, config);
		}

		if (MutationObserver){
			if (0 > interval) {
				initInterval();
			} else {
				createMutationObserver();
			}
		}
		else {
			warn('MutationObserver not supported in this browser!');
			initInterval();
		}
	}


	// document.documentElement.offsetHeight is not reliable, so
	// we have to jump through hoops to get a better value.
	function getBodyOffsetHeight(){
		function getComputedBodyStyle(prop) {
			function convertUnitsToPxForIE8(value) {
				var PIXEL = /^\d+(px)?$/i;

				if (PIXEL.test(value)) {
					return parseInt(value,base);
				}

				var
					style = el.style.left,
					runtimeStyle = el.runtimeStyle.left;

				el.runtimeStyle.left = el.currentStyle.left;
				el.style.left = value || 0;
				value = el.style.pixelLeft;
				el.style.left = style;
				el.runtimeStyle.left = runtimeStyle;

				return value;
			}

			var
				el = document.body,
				retVal = 0;

			if (('defaultView' in document) && ('getComputedStyle' in document.defaultView)) {
				retVal = document.defaultView.getComputedStyle(el, null);
				retVal = (null !== retVal) ? retVal[prop] : 0;
			} else {//IE8
				retVal =  convertUnitsToPxForIE8(el.currentStyle[prop]);
			}

			return parseInt(retVal,base);
		}

		return  document.body.offsetHeight +
				getComputedBodyStyle('marginTop') +
				getComputedBodyStyle('marginBottom');
	}

	function getBodyScrollHeight(){
		return document.body.scrollHeight;
	}

	function getDEOffsetHeight(){
		return document.documentElement.offsetHeight;
	}

	function getDEScrollHeight(){
		return document.documentElement.scrollHeight;
	}

	//From https://github.com/guardian/iframe-messenger
	function getLowestElementHeight() {
		var
			allElements       = document.querySelectorAll('body *'),
			allElementsLength = allElements.length,
			maxBottomVal      = 0,
			timer             = new Date().getTime();

		for (var i = 0; i < allElementsLength; i++) {
			if (allElements[i].getBoundingClientRect().bottom > maxBottomVal) {
				maxBottomVal = allElements[i].getBoundingClientRect().bottom;
			}
		}

		timer = new Date().getTime() - timer;

		log('Parsed '+allElementsLength+' HTML elements');
		log('LowestElement bottom position calculated in ' + timer + 'ms');

		return maxBottomVal;
	}

	function getAllHeights(){
		return [
			getBodyOffsetHeight(),
			getBodyScrollHeight(),
			getDEOffsetHeight(),
			getDEScrollHeight()
		];
	}

	function getMaxHeight(){
		return Math.max.apply(null,getAllHeights());
	}

	function getMinHeight(){
		return Math.min.apply(null,getAllHeights());
	}

	function getBestHeight(){
		return Math.max(getBodyOffsetHeight(),getLowestElementHeight());
	}

	var getHeight = {
		offset                : getBodyOffsetHeight, //Backward compatability
		bodyOffset            : getBodyOffsetHeight,
		bodyScroll            : getBodyScrollHeight,
		documentElementOffset : getDEOffsetHeight,
		scroll                : getDEScrollHeight, //Backward compatability
		documentElementScroll : getDEScrollHeight,
		max                   : getMaxHeight,
		min                   : getMinHeight,
		grow                  : getMaxHeight,
		lowestElement         : getBestHeight
	};

	function getWidth(){
		return Math.max(
			document.documentElement.scrollWidth,
			document.body.scrollWidth
		);
	}

	function sendSize(triggerEvent, triggerEventDesc, customHeight, customWidth){

		var	currentHeight,currentWidth;

		function recordTrigger(){
			if (!(triggerEvent in {'reset':1,'resetPage':1,'init':1})){
				log( 'Trigger event: ' + triggerEventDesc );
			}
		}

		function resizeIFrame(){
			height = currentHeight;
			width  = currentWidth;

			sendMsg(height,width,triggerEvent);
		}

		function isDoubleFiredEvent(){
			return  triggerLocked && (triggerEvent in doubleEventList);
		}

		function isSizeChangeDetected(){
			function checkTolarance(a,b){
				var retVal = Math.abs(a-b) <= tolerance;
				return !retVal;
			}

			currentHeight = (undefined !== customHeight)  ? customHeight : getHeight[heightCalcMode]();
			currentWidth  = (undefined !== customWidth )  ? customWidth  : getWidth();

			return	checkTolarance(height,currentHeight) ||
					(calculateWidth && checkTolarance(width,currentWidth));
		}

		function isForceResizableEvent(){
			return !(triggerEvent in {'init':1,'interval':1,'size':1});
		}

		function isForceResizableHeightCalcMode(){
			return (heightCalcMode in resetRequiredMethods);
		}

		function logIgnored(){
			log('No change in size detected');
		}

		function checkDownSizing(){
			if (isForceResizableEvent() && isForceResizableHeightCalcMode()){
				resetIFrame(triggerEventDesc);
			} else if (!(triggerEvent in {'interval':1})){
				recordTrigger();
				logIgnored();
			}
		}

		if (!isDoubleFiredEvent()){
			if (isSizeChangeDetected()){
				recordTrigger();
				lockTrigger();
				resizeIFrame();
			} else {
				checkDownSizing();
			}
		} else {
			log('Trigger event cancelled: '+triggerEvent);
		}
	}

	function lockTrigger(){
		if (!triggerLocked){
			triggerLocked = true;
			log('Trigger event lock on');
		}
		clearTimeout(triggerLockedTimer);
		triggerLockedTimer = setTimeout(function(){
			triggerLocked = false;
			log('Trigger event lock off');
			log('--');
		},eventCancelTimer);
	}

	function triggerReset(triggerEvent){
		height = getHeight[heightCalcMode]();
		width  = getWidth();

		sendMsg(height,width,triggerEvent);
	}

	function resetIFrame(triggerEventDesc){
		var hcm = heightCalcMode;
		heightCalcMode = heightCalcModeDefault;

		log('Reset trigger event: ' + triggerEventDesc);
		lockTrigger();
		triggerReset('reset');

		heightCalcMode = hcm;
	}

	function sendMsg(height,width,triggerEvent,msg,targetOrigin){
		function setTargetOrigin(){
			if (undefined === targetOrigin){
				targetOrigin = targetOriginDefault;
			} else {
				log('Message targetOrigin: '+targetOrigin);
			}
		}

		function sendToParent(){
			var
				size  = height + ':' + width,
				message = myID + ':' +  size + ':' + triggerEvent + (undefined !== msg ? ':' + msg : '');

			log('Sending message to host page (' + message + ')');
			target.postMessage( msgID + message, targetOrigin);
		}

		setTargetOrigin();
		sendToParent();
	}

	function receiver(event) {
		function isMessageForUs(){
			return msgID === (''+event.data).substr(0,msgIdLen); //''+ Protects against non-string messages
		}

		function initFromParent(){
			initMsg = event.data;
			target  = event.source;

			init();
			firstRun = false;
			setTimeout(function(){ initLock = false;},eventCancelTimer);
		}

		function resetFromParent(){
			if (!initLock){
				log('Page size reset by host page');
				triggerReset('resetPage');
			} else {
				log('Page reset ignored by init');
			}
		}

		function getMessageType(){
			return event.data.split(']')[1];
		}

		function isMiddleTier(){
			return ('iFrameResize' in window);
		}

		function isInitMsg(){
			//test if this message is from a child below us. This is an ugly test, however, updating
			//the message format would break backwards compatibity.
			return event.data.split(':')[2] in {'true':1,'false':1};
		}

		if (isMessageForUs()){
			if (firstRun && isInitMsg()){ //Check msg ID
				initFromParent();
			} else if ('reset' === getMessageType()){
				resetFromParent();
			} else if (event.data !== initMsg && !isMiddleTier()){
				warn('Unexpected message ('+event.data+')');
			}
		}
	}

	addEventListener(window, 'message', receiver);

})();
